import { act, render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';

import { ComponentClassName } from '@aws-amplify/ui';

import { StepperField } from '../StepperField';
import {
  testFlexProps,
  expectFlexContainerStyleProps,
} from '../../Flex/__tests__/Flex.test';
import { ComponentText } from '../../shared/constants';
import { AUTO_GENERATED_ID_PREFIX } from '../../utils/useStableId';
import { ERROR_SUFFIX, DESCRIPTION_SUFFIX } from '../../../helpers/constants';

const LABEL = 'stepper';

describe('StepperField:', () => {
  describe('Flex wrapper', () => {
    it('should render default and custom classname', async () => {
      const classname = 'test-class';
      render(
        <StepperField
          label="stepper"
          testId="stepper-field"
          className={classname}
        />
      );

      const stepperField = await screen.findByTestId('stepper-field');
      expect(stepperField).toHaveClass(
        ComponentClassName.Field,
        ComponentClassName.StepperField,
        classname
      );
    });

    it('should set size attribute', async () => {
      render(
        <StepperField label="stepper" testId="stepper-field" size="small" />
      );
      const stepperField = await screen.findByTestId('stepper-field');
      expect(stepperField).toHaveClass(`${ComponentClassName.Field}--small`);
    });

    it('should render size classes for StepperField', async () => {
      render(
        <div>
          <StepperField label="stepper" testId="small" size="small" />
          <StepperField label="stepper" testId="large" size="large" />
        </div>
      );

      const small = await screen.findByTestId('small');
      const large = await screen.findByTestId('large');

      expect(small.classList).toContain(
        `${ComponentClassName['Field']}--small`
      );
      expect(large.classList).toContain(
        `${ComponentClassName['Field']}--large`
      );
    });

    it('should render all flex style props', async () => {
      render(
        <StepperField
          testId="stepper-field"
          label="stepper"
          {...testFlexProps}
        />
      );
      const stepperField = await screen.findByTestId('stepper-field');
      expectFlexContainerStyleProps(stepperField);
    });
  });

  describe('Label', () => {
    it('should render expected field classname', async () => {
      render(<StepperField label="stepper" />);

      const stepperLabel = await screen.findByText('stepper');
      expect(stepperLabel).toHaveClass(ComponentClassName.Label);
    });

    it('should have `amplify-visually-hidden` class when labelHidden is true', async () => {
      render(<StepperField label="stepper" labelHidden />);

      const stepperLabel = await screen.findByText('stepper');
      expect(stepperLabel).toHaveClass('amplify-visually-hidden');
    });
  });

  describe('Input field', () => {
    it('should render classname', async () => {
      render(<StepperField label={LABEL} />);
      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput).toHaveClass(ComponentClassName.StepperFieldInput);
    });

    it('should forward ref to DOM element', async () => {
      const ref = React.createRef<HTMLInputElement>();
      render(<StepperField label={LABEL} ref={ref} />);

      await screen.findByLabelText(LABEL);
      expect(ref.current?.nodeName).toBe('INPUT');
    });

    it('should render labeled input when id is provided', async () => {
      render(<StepperField label={LABEL} id="stepper-field" />);
      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput.id).toBe('stepper-field');
    });

    it('should render labeled input when id is not provided, and is autogenerated', async () => {
      render(<StepperField label={LABEL} />);
      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput.id.startsWith(AUTO_GENERATED_ID_PREFIX)).toBeTruthy();
    });

    it('should set value correctly (controlled)', async () => {
      const { rerender } = render(
        <StepperField label={LABEL} min={-10} max={10} step={2} value={0} />
      );

      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput).toHaveValue(0);

      rerender(
        <StepperField label={LABEL} min={-10} max={10} step={2} value={-2} />
      );

      expect(stepperInput).toHaveValue(-2);

      rerender(
        <StepperField label={LABEL} min={-10} max={10} step={2} value={0} />
      );

      expect(stepperInput).toHaveValue(0);

      await act(async () => {
        await userEvent.type(stepperInput, '9');
      });
      fireEvent.blur(stepperInput);
      // will be rounded up to 10 when losing focus since the step is 2
      expect(stepperInput).toHaveValue(10);
    });

    it('should set value correctly (uncontrolled)', async () => {
      render(
        <StepperField
          label={LABEL}
          defaultValue={0}
          min={-10}
          max={10}
          step={2}
        />
      );
      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput).toHaveValue(0);
      const buttons = await screen.findAllByRole('button');
      await act(async () => {
        await userEvent.click(buttons[0]);
      });
      expect(stepperInput).toHaveValue(-2);
      await act(async () => {
        await userEvent.click(buttons[1]);
      });
      expect(stepperInput).toHaveValue(0);
      await act(async () => {
        await userEvent.type(stepperInput, '9');
      });
      fireEvent.blur(stepperInput);
      // will be rounded up to 10 when losing focus since the step is 2
      expect(stepperInput).toHaveValue(10);
    });

    it('should render min, max, step attributes', async () => {
      render(<StepperField label={LABEL} min={0} max={10} step={2} />);

      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput).toHaveAttribute('min', '0');
      expect(stepperInput).toHaveAttribute('max', '10');
      expect(stepperInput).toHaveAttribute('step', '2');
    });

    it('should render the state attributes', async () => {
      render(<StepperField label={LABEL} isDisabled isReadOnly isRequired />);

      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput).toHaveAttribute('disabled');
      expect(stepperInput).toHaveAttribute('readonly');
      expect(stepperInput).toHaveAttribute('required');
    });

    it('should set size attribute', async () => {
      render(<StepperField label={LABEL} size="small" />);

      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput).toHaveClass(`${ComponentClassName.Input}--small`);
    });

    it('show render aria-invalid attribute to input when hasError', async () => {
      render(
        <StepperField label={LABEL} errorMessage="Error message" hasError />
      );
      const stepperInput = await screen.findByLabelText(LABEL);
      expect(stepperInput).toHaveAttribute('aria-invalid', 'true');
    });

    it('should fire event handlers', async () => {
      const onBlur = jest.fn();
      const onChange = jest.fn();
      const onWheel = jest.fn();
      render(
        <StepperField
          label={LABEL}
          onBlur={onBlur}
          onChange={onChange}
          onWheel={onWheel}
        />
      );
      const stepperInput = await screen.findByLabelText(LABEL);
      await act(async () => {
        await userEvent.type(stepperInput, '100');
      });
      fireEvent.blur(stepperInput);
      fireEvent.wheel(stepperInput);
      expect(onChange).toHaveBeenCalled();
      expect(onBlur).toHaveBeenCalled();
      expect(onWheel).toHaveBeenCalled();
    });
  });

  describe('Increase/Decrease button', () => {
    it('should pass through size attribute', async () => {
      render(<StepperField label="stepper" size="small" />);
      const buttons = await screen.findAllByRole('button');
      expect(buttons[0]).toHaveClass(`${ComponentClassName.Button}--small`);
      expect(buttons[1]).toHaveClass(`${ComponentClassName.Button}--small`);
    });

    it('should render the size classes for StepperField', async () => {
      render(
        <div>
          <StepperField label="stepper" size="small" />
          <StepperField label="stepper" size="large" />
        </div>
      );
      const buttons = await screen.findAllByRole('button');
      expect(buttons[0]).toHaveClass(`${ComponentClassName.Button}--small`);
      expect(buttons[1]).toHaveClass(`${ComponentClassName.Button}--small`);
      expect(buttons[2]).toHaveClass(`${ComponentClassName.Button}--large`);
      expect(buttons[3]).toHaveClass(`${ComponentClassName.Button}--large`);
    });

    it('should render the variation classes for StepperField', async () => {
      render(
        <div>
          <StepperField label="stepper" variation="quiet" />
        </div>
      );
      const buttons = await screen.findAllByRole('button');
      expect(buttons[0]).toHaveClass(
        `${ComponentClassName.StepperFieldButtonDecrease}--quiet`
      );
      expect(buttons[1]).toHaveClass(
        `${ComponentClassName.StepperFieldButtonIncrease}--quiet`
      );
    });

    it('should render aria attributes', async () => {
      const id = 'stepper-field';
      render(
        <StepperField
          label="stepper"
          id={id}
          defaultValue={0}
          min={0}
          max={10}
          step={2}
        />
      );
      const buttons = await screen.findAllByRole('button');
      expect(buttons[0]).toHaveAttribute(
        'aria-label',
        `${ComponentText.StepperField.decreaseButtonLabel} -2`
      );
      expect(buttons[1]).toHaveAttribute(
        'aria-label',
        `${ComponentText.StepperField.increaseButtonLabel} 2`
      );
      expect(buttons[0]).toHaveAttribute('aria-controls', id);
      expect(buttons[1]).toHaveAttribute('aria-controls', id);
    });

    it('should be able to customize aria label', async () => {
      const id = 'stepper-field';
      const increaseButtonLabel = 'Custom increase to';
      const decreaseButtonLabel = 'Custom decrease to';
      render(
        <StepperField
          label="stepper"
          increaseButtonLabel={increaseButtonLabel}
          decreaseButtonLabel={decreaseButtonLabel}
          id={id}
          defaultValue={0}
          min={0}
          max={10}
          step={2}
        />
      );

      const buttons = await screen.findAllByRole('button');
      expect(buttons[0]).toHaveAttribute(
        'aria-label',
        `${decreaseButtonLabel} -2`
      );
      expect(buttons[1]).toHaveAttribute(
        'aria-label',
        `${increaseButtonLabel} 2`
      );
    });
  });

  describe('Error messages', () => {
    const errorMessage = 'This is an error message';
    it('should not show when hasError is false', () => {
      render(<StepperField label="stepper" errorMessage={errorMessage} />);

      const errorText = screen.queryByText(errorMessage);
      expect(errorText).not.toBeInTheDocument();
    });

    it('show when hasError and errorMessage', () => {
      render(
        <StepperField label="stepper" errorMessage={errorMessage} hasError />
      );
      const errorText = screen.queryByText(errorMessage);
      expect(errorText?.innerHTML).toContain(errorMessage);
    });
  });

  describe('descriptive message', () => {
    it('should render descriptiveText if it is provided', () => {
      render(<StepperField label="stepper" descriptiveText="Description" />);

      const descriptiveText = screen.queryByText('Description');
      expect(descriptiveText?.innerHTML).toContain('Description');
    });

    it('should map to descriptive text correctly', async () => {
      render(<StepperField label="stepper" descriptiveText="Description" />);

      const stepperInput = await screen.findByLabelText('stepper');
      expect(stepperInput).toHaveAccessibleDescription('Description');
    });
  });

  describe('aria-describedby test', () => {
    const errorMessage = 'This is an error message';
    const descriptiveText = 'Description';
    it('when hasError, include id of error component and describe component in the aria-describedby', async () => {
      render(
        <StepperField
          label="stepper"
          descriptiveText={descriptiveText}
          errorMessage={errorMessage}
          hasError
        />
      );

      const stepperInput = await screen.findByLabelText(LABEL);
      const ariaDescribedBy = stepperInput.getAttribute('aria-describedby');
      const descriptiveTextElement = screen.queryByText(descriptiveText);
      const errorTextElement = screen.queryByText(errorMessage);
      expect(
        errorTextElement?.id && errorTextElement?.id.endsWith(ERROR_SUFFIX)
      ).toBe(true);
      expect(
        descriptiveTextElement?.id &&
          descriptiveTextElement?.id.endsWith(DESCRIPTION_SUFFIX)
      ).toBe(true);
      expect(
        errorTextElement?.id &&
          descriptiveTextElement?.id &&
          ariaDescribedBy ===
            `${errorTextElement.id} ${descriptiveTextElement.id}`
      ).toBe(true);
    });

    it('only show id of describe component in aria-describedby when hasError is false', async () => {
      render(
        <StepperField
          label="stepper"
          descriptiveText={descriptiveText}
          errorMessage={errorMessage}
        />
      );

      const stepperInput = await screen.findByLabelText(LABEL);
      const ariaDescribedBy = stepperInput.getAttribute('aria-describedby');
      const descriptiveTextElement = screen.queryByText(descriptiveText);
      expect(
        descriptiveTextElement?.id &&
          ariaDescribedBy?.startsWith(descriptiveTextElement?.id)
      ).toBe(true);
    });

    it('aria-describedby should be empty when hasError is false and descriptiveText is empty', async () => {
      render(<StepperField label="stepper" errorMessage={errorMessage} />);
      const stepperInput = await screen.findByLabelText(LABEL);
      const ariaDescribedBy = stepperInput.getAttribute('aria-describedby');
      expect(ariaDescribedBy).toBeNull();
    });
  });
});
